#pragma once

#include <string>
#include <memory>
#include "common/str.h"
#include "common/json.h"
#include "cmdline.h"


namespace evm { namespace config {

using namespace std;
using Parser = parser;

struct Group {
    Group(string name) : name(std::move(name)) {}

    const string& operator()() const {
        return name;
    }

    friend bool operator==(const Group& lhs, const Group& rhs) {
        return lhs.name == rhs.name;
    }

private:
    string name;
};

extern const Group DEFAULT_GROUP;

struct ConfigOpts : public Json {

    ConfigOpts(string program = "", string default_config_file = "");

    void set_default_config_file(string default_config_file) {
        this->default_config_file = default_config_file;
    }

    template <typename Type>
    void register_opt(string name, Type default_, const char* help,
                      const Group& group = DEFAULT_GROUP) {
        register_opt(std::move(name), std::move(default_), false, help, group, false);
    }

    template <typename Type>
    void register_opt(string name, const char* help,
                      const Group& group = DEFAULT_GROUP) {
        register_opt(std::move(name), std::move(Type()), true, help, group, false);
    }

    template <typename Type>
    void register_cli_opt(string name, Type default_, const char* help,
                          const Group& group = DEFAULT_GROUP) {
        register_cli_opt(std::move(name), std::move(default_), false, help, group);
    }

    template <typename Type>
    void register_cli_opt(string name, const char* help,
                          const Group& group = DEFAULT_GROUP) {
        register_cli_opt(std::move(name), std::move(Type()), true, help, group);
    }

    template <typename Type>
    void register_opt(string name, Type default_ = Type(), bool required = false, string help = "",
                      const Group& group = DEFAULT_GROUP, bool cli = false)
    {
        if (name == "_meta")
            throw invalid_argument("confilcts with internel option name _meta");

        auto& group_name = group();

        if (group_name == DEFAULT_GROUP()) {
            if (this->count(name))
                throw invalid_argument("conflicts with existed option name or group name '" + name + "'");

            (*this)[name] = default_;  // option in default group can be access directly
        }

        if (this->count(group_name) && (*this)[group_name].count(name))
            throw invalid_argument("conflicts with existed option name '" + name + "' in group '" + group_name + "'");

        (*this)[group_name][name] = default_;

        (*this)["_meta"][group_name][name] = {
            {"default", default_},
            {"required", required},
            {"help", help},
            {"cli", cli}
        };
    }

    template <typename Type>
    void register_cli_opt(string name, Type default_ = Type(), bool required = true, string help = "",
                          const Group& group = DEFAULT_GROUP) {
        string cli_name = cli_opt_name(name, group);
        cli.add<Type>(cli_name, '\0', help, required, default_);
        register_opt(std::move(name), std::move(default_), required, std::move(help), group, true);
    }

    template <typename Type>
    void override_opt(string name, Type value, const Group& group = DEFAULT_GROUP) {
        if (!count(group()))
            throw invalid_argument("group '" + group() + "' does not exist.");
        if (!(*this)[group()].count(name))
            throw invalid_argument("group '" + group() + "' has no option '" + name + "'.");

        //TODO: check new value type

        (*this)[group()][name] = value;

        if (group == DEFAULT_GROUP)
            (*this)[name] = value;
    }

    // access group by group name
    Json& group(string group_name = DEFAULT_GROUP()) {
        return (*this)[group_name];
    }

    // parse command line arguments and configuration files
    void operator()(int argc = 0, const char** argv = nullptr);

    void parse_args(int argc = 0, const char** argv = nullptr) {
        (*this)(argc, argv);
    }

    operator bool() {
        return is_parsed;
    }

    void log() const;

private:
    static string cli_opt_name(string opt_name, const Group& group = DEFAULT_GROUP) {
        const auto& group_name = group();
        if (group_name == DEFAULT_GROUP())
            return opt_name;
        else
            return group_name + "_" + opt_name;
    }
    
    string program;
    string default_config_file;
    Parser cli;
    bool is_parsed{};
};

using ConfigOptsPtr = shared_ptr<ConfigOpts>;

}}
